package com.jadn.cc.services; import java.io.File; import java.io.FileOutputStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Date; import java.util.List; import java.util.SortedSet; import android.app.Activity; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.*; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.media.AudioManager; import android.media.MediaMetadataRetriever; import android.media.RemoteControlClient; import android.net.wifi.WifiManager; import android.os.Binder; import android.os.Bundle; import android.os.IBinder; import android.os.PowerManager; import android.os.PowerManager.WakeLock; import android.preference.PreferenceManager; import android.telephony.PhoneStateListener; import android.telephony.TelephonyManager; import android.util.Log; import android.view.KeyEvent; import android.widget.Toast; import com.aocate.media.MediaPlayer; import com.jadn.cc.R; import com.jadn.cc.core.AudioFocusHelper; import com.jadn.cc.core.CarCastApplication; import com.jadn.cc.core.Config; import com.jadn.cc.core.Location; import com.jadn.cc.core.MediaMode; import com.jadn.cc.core.MusicFocusable; import com.jadn.cc.core.Subscription; import com.jadn.cc.core.WifiConnectedReceiver; import com.jadn.cc.trace.TraceUtil; import com.jadn.cc.ui.CarCast; import com.jadn.cc.util.ExportOpml; import com.jadn.cc.util.MailRecordings; public class ContentService extends Service implements MediaPlayer.OnCompletionListener, MusicFocusable { private final IBinder binder = new LocalBinder(); int currentPodcastInPlayer = -1; DownloadHelper downloadHelper; Location location; MediaMode mediaMode = MediaMode.UnInitialized; MediaPlayer mediaPlayer = null; MetaHolder metaHolder; SearchHelper searchHelper; // Dummy album art we will pass to the remote control (if the APIs are available). Bitmap mDummyAlbumArt; private PlayStatusListener playStatusListener; RemoteControlClientCompat mRemoteControlClientCompat; private Context context; private Config config; FileSubscriptionHelper subHelper; enum PauseReason { PhoneCall, UserRequest, // paused by user request FocusLoss, // paused because of audio focus loss } ; // why did we pause? PauseReason mPauseReason = PauseReason.UserRequest; // do we have audio focus? enum AudioFocus { NoFocusNoDuck, // we don't have audio focus, and can't duck NoFocusCanDuck, // we don't have focus, but can play at a low volume ("ducking") Focused // we have full audio focus } AudioFocus mAudioFocus = AudioFocus.NoFocusNoDuck; // Lifted from RandomMusicPlayer google reference application // our AudioFocusHelper object, if it's available (it's available on SDK level >= 8) // If not available, this will be null. Always check for null before using! AudioFocusHelper mAudioFocusHelper = null; public void setApplicationContext(Context context) { this.context = context; try { if (mediaPlayer == null) { mediaPlayer = new MediaPlayer(context, true); fullReset(); } } catch (Exception e) { Log.d("CarCast", "Error doing reset", e); } } /** * Class for clients to access. Because we know this service always runs in the same process as its clients, we * don't need to deal with IPC. */ public class LocalBinder extends Binder { public ContentService getService() { return ContentService.this; } } public static String getTimeString(int time) { StringBuilder sb = new StringBuilder(); int min = time / (1000 * 60); if (min < 10) sb.append('0'); sb.append(min); sb.append(':'); int sec = (time - min * 60 * 1000) / 1000; if (sec < 10) sb.append('0'); sb.append(sec); return sb.toString(); } public boolean addSubscription(Subscription toAdd) { return subHelper.addSubscription(toAdd); } public void bumpForwardSeconds(int bump) { if (currentPodcastInPlayer >= metaHolder.getSize()) return; try { int npos = mediaPlayer.getCurrentPosition() + bump * 1000; if (npos < 0) { npos = 0; } else if (npos > mediaPlayer.getDuration()) { npos = mediaPlayer.getDuration() - 1; mediaPlayer.seekTo(npos); } mediaPlayer.seekTo(npos); } catch (Exception e) { // do nothing } if (!mediaPlayer.isPlaying()) { saveState(); } } public MetaFile currentMeta() { if (metaHolder.getSize() == 0) return null; if (currentPodcastInPlayer == -1) return null; if (metaHolder.getSize() <= currentPodcastInPlayer) return null; return metaHolder.get(currentPodcastInPlayer); } private int currentDuration() { if (currentPodcastInPlayer >= metaHolder.getSize()) { return 0; } int dur = currentMeta().getDuration(); if (dur != -1) return dur; if (mediaMode == MediaMode.UnInitialized) { currentMeta().computeDuration(); return currentMeta().getDuration(); } return currentMeta().getDuration(); } public File currentFile() { MetaFile meta = currentMeta(); return meta == null ? null : meta.file; } int currentPostion() { if (currentPodcastInPlayer >= metaHolder.getSize()) { return 0; } return metaHolder.get(currentPodcastInPlayer).getCurrentPos(); } public int currentProgress() { if (mediaMode == MediaMode.UnInitialized) { int duration = currentDuration(); if (duration == 0) return 0; return currentPostion() * 100 / duration; } if (mediaPlayer.getDuration() == 0) { return 0; } return mediaPlayer.getCurrentPosition() * 100 / mediaPlayer.getDuration(); } public CharSequence currentSummary() { StringBuilder sb = new StringBuilder(); if (currentPodcastInPlayer >= metaHolder.getSize()) { if (isDownloading()) return "Downloading podcasts"; return "No Podcasts have been downloaded."; } sb.append(currentMeta().getFeedName()); sb.append('\n'); sb.append(currentMeta().getTitle()); return sb.toString(); } boolean isDownloading() { if (downloadHelper == null) return false; return downloadHelper.isRunning(); } public String currentTitle() { if (currentPodcastInPlayer >= metaHolder.getSize()) { if (isDownloading()) { return "Downloading podcasts\n" + downloadHelper.getStatus(); } return "No podcasts loaded.\nUse 'Menu' and 'Download Podcasts'"; } return currentMeta().getTitle(); } public void deleteAllSubscriptions() { subHelper.deleteAllSubscriptions(); } // used by status when mediaplayer is not started. public void deleteCurrentPodcast() { if (mediaMode == MediaMode.Playing) { pauseNow(); } metaHolder.get(currentPodcastInPlayer).delete(); metaHolder = new MetaHolder(getApplicationContext(), currentFile()); if (currentPodcastInPlayer >= metaHolder.getSize()) { currentPodcastInPlayer = 0; } updateRemoteDisplay(); } public void deletePodcast(int position) { if (mediaPlayer.isPlaying() && currentPodcastInPlayer == position) { pauseNow(); } metaHolder.delete(position); if (currentPodcastInPlayer >= metaHolder.getSize()) { if (currentPodcastInPlayer > 0) currentPodcastInPlayer--; } // If we are playing something after what's deleted, adjust the current if (currentPodcastInPlayer > position) currentPodcastInPlayer--; updateRemoteDisplay(); try { fullReset(); } catch (Throwable e) { // bummer. } } public void deleteSubscription(Subscription sub) { subHelper.removeSubscription(sub); } public void toggleSubscription(Subscription sub) { subHelper.toggleSubscription(sub); } void deleteUpTo(int upTo) { if (mediaPlayer.isPlaying()) { pauseNow(); mediaPlayer.stop(); mediaPlayer.reset(); } mediaMode = MediaMode.UnInitialized; if (upTo == -1) upTo = metaHolder.getSize(); for (int i = 0; i < upTo; i++) { metaHolder.delete(0); } metaHolder = new MetaHolder(getApplicationContext(), currentFile()); tryToRestoreLocation(); if (location == null) currentPodcastInPlayer = 0; } void doDownloadCompletedNotification(int got) { // Get the notification manager service. NotificationManager mNotificationManager = (NotificationManager) getSystemService(Activity.NOTIFICATION_SERVICE); mNotificationManager.cancel(23); // Allow UI to update download text (only when in debug mode) this seems // suboptimal try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } SharedPreferences app_preferences = PreferenceManager.getDefaultSharedPreferences(this); if (got == 0 && !app_preferences.getBoolean("notifiyOnZeroDownloads", true)) { mNotificationManager.cancel(22); } else { Notification notification = new Notification(R.drawable.icon2, "Download complete", System.currentTimeMillis()); PendingIntent contentIntent = PendingIntent.getActivity(this, 0, new Intent(this, CarCast.class), 0); notification.setLatestEventInfo(getBaseContext(), "Downloads Finished", "Downloaded " + got + " podcasts.", contentIntent); notification.flags = Notification.FLAG_AUTO_CANCEL; mNotificationManager.notify(22, notification); } metaHolder = new MetaHolder(getApplicationContext(), currentFile()); if (currentPodcastInPlayer >= metaHolder.getSize()) { currentPodcastInPlayer = 0; } updateRemoteDisplay(); } public boolean editSubscription(Subscription original, Subscription modified) { return subHelper.editSubscription(original, modified); } public String encodedDownloadStatus() { if (downloadHelper == null) { return ""; } return downloadHelper.getEncodedStatus(); } private boolean fullReset() throws Exception { if (mediaPlayer != null) { mediaPlayer.reset(); } applyVariableSpeedProperties(); if (currentPodcastInPlayer >= metaHolder.getSize()) return false; mediaPlayer.setDataSource(currentFile().toString()); mediaPlayer.prepare(); mediaPlayer.setOnCompletionListener(this); mediaPlayer.seekTo(metaHolder.get(currentPodcastInPlayer).getCurrentPos()); return true; } private void applyVariableSpeedProperties() { SharedPreferences app_preferences = PreferenceManager.getDefaultSharedPreferences(context); boolean variableSpeed = app_preferences.getBoolean("variableSpeedEnabled", false); mediaPlayer.setUseService(variableSpeed); mediaPlayer.setEnableSpeedAdjustment(variableSpeed); Log.d("CarCast", "SPEED CHOICE " + app_preferences.getString("speedChoice", "undefined")); Float speed = null; try { speed = Float.parseFloat(app_preferences.getString("speedChoice", "1.75")); } catch (Exception e) { Log.d("CarCast", e.getMessage()); speed = 1.75f; } mediaPlayer.setPlaybackSpeed(speed); } public int getCount() { return metaHolder.getSize(); } public String getCurrentSubscriptionName() { if (currentPodcastInPlayer >= metaHolder.getSize()) { return ""; } return currentMeta().getFeedName(); } public String getDurationString() { return getTimeString(currentDuration()); } public Location getLocation() { if (mediaMode == MediaMode.UnInitialized) { return new Location(currentTitle()); } return new Location(currentTitle()); } public String getLocationString() { Location useLocation = getLocation(); if (isPlaying()) { return getTimeString(mediaPlayer.getCurrentPosition()); } if (currentMeta() != null) return getTimeString(currentMeta().getCurrentPos()); return ""; } public MediaMode getMediaMode() { return mediaMode; } public String getPodcastEmailSummary() { StringBuilder sb = new StringBuilder(); if (currentPodcastInPlayer < metaHolder.getSize()) { MetaFile mf = currentMeta(); if (mf != null) { sb.append("\nWanted to let you know about this podcast:\n\n"); sb.append("\nTitle: " + mf.getTitle()); String searchName = mf.getFeedName(); sb.append("\nFeed Title: " + searchName); List<Subscription> subs = getSubscriptions(); for (Subscription sub : subs) { if (sub.name.equals(searchName)) { sb.append("\nFeed URL: " + sub.url); break; } } if (mf.getUrl() != null) { sb.append("\nPodcast URL: " + mf.getUrl()); } } } sb.append("\n\n\n"); sb.append("This email sent from " + CarCastApplication.getAppTitle() + "."); return sb.toString(); } /** * Gets a Map of URLs to Subscription Name * * @return a map keyed on sub url to value of sub name */ public List<Subscription> getSubscriptions() { List<Subscription> subscriptions = subHelper.getSubscriptions(); return subscriptions; } public String getWhereString() { StringBuilder sb = new StringBuilder(); if (metaHolder.getSize() == 0) sb.append('0'); else sb.append(currentPodcastInPlayer + 1); sb.append('/'); sb.append(metaHolder.getSize()); return sb.toString(); } public void moveTo(double d) { if (mediaMode == MediaMode.UnInitialized) { if (currentDuration() == 0) return; metaHolder.get(currentPodcastInPlayer).setCurrentPos((int) (d * currentDuration())); mediaPlayer.reset(); try { mediaPlayer.setDataSource(currentFile().toString()); mediaPlayer.prepare(); } catch (Exception e) { TraceUtil.report(e); } mediaMode = MediaMode.Paused; return; } mediaPlayer.seekTo((int) (d * mediaPlayer.getDuration())); } // Called when user hits next button. Migth be playing or not playing at the // time. public void next() { boolean isPlaying = mediaPlayer.isPlaying(); if (isPlaying) { currentMeta().setCurrentPos(mediaPlayer.getCurrentPosition()); currentMeta().save(); mediaPlayer.stop(); } next(isPlaying); } // called when user hits button (might be playing or not playing) and called // when // the playback engine his the "onCompletion" event (ie. a podcast has // finished, in which case // we are actually no longer playing but we were just were a millisecond or // so ago.) void next(boolean inTheActOfPlaying) { mediaMode = MediaMode.UnInitialized; // if we are at end. if (currentPodcastInPlayer + 1 >= metaHolder.getSize()) { saveState(); // activity.disableJumpButtons(); mediaPlayer.reset(); // say(activity, "That's all folks"); if (inTheActOfPlaying) disableNotification(); return; } currentPodcastInPlayer++; updateRemoteDisplay(); if (inTheActOfPlaying) play(); else disableNotification(); } @Override public IBinder onBind(Intent intent) { Log.i("CarCast", "ContentService binding " + intent); return binder; } @Override public boolean onUnbind(Intent intent) { Log.i("CarCast", "ContentService unbinding " + intent); return super.onUnbind(intent); } @Override public void onCompletion(MediaPlayer mp) { if (currentMeta() == null) return; currentMeta().setCurrentPos(0); currentMeta().setListenedTo(); currentMeta().save(); if (PreferenceManager.getDefaultSharedPreferences(getApplicationContext()).getBoolean("autoPlayNext", true)) { next(true); } else { disableNotification(); } } private void initDirs() { File legacyFile = config.getCarCastPath("podcasts.txt"); File siteListFile = config.getCarCastPath("podcasts.properties"); subHelper = new FileSubscriptionHelper(siteListFile, legacyFile); } @Override public void onCreate() { super.onCreate(); WifiConnectedReceiver.registerForWifiBroadcasts(getApplicationContext()); config = new Config(getApplicationContext()); initDirs(); // Google handles surprise exceptions now, so we dont have to. //ExceptionHandler.register(this); partialWakeLock = ((PowerManager) getSystemService(Context.POWER_SERVICE)).newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, CarCastApplication.getAppTitle()); partialWakeLock.setReferenceCounted(false); PhoneStateListener phoneStateListener = new PhoneStateListener() { @Override public void onCallStateChanged(int state, String incomingNumber) { super.onCallStateChanged(state, incomingNumber); if (state == TelephonyManager.CALL_STATE_OFFHOOK || state == TelephonyManager.CALL_STATE_RINGING) { // It's possible that this listener is registered before the setApplicationContext // method is called, which establishes the MediaPlayer instance. if (mediaPlayer != null && mediaPlayer.isPlaying()) { mPauseReason = PauseReason.PhoneCall; pauseNow(); bumpForwardSeconds(-5); } } if (state == TelephonyManager.CALL_STATE_IDLE && mPauseReason == PauseReason.PhoneCall) { mPauseReason = PauseReason.UserRequest; pauseOrPlay(); } } }; final TelephonyManager telMgr = (TelephonyManager) this.getSystemService(Context.TELEPHONY_SERVICE); telMgr.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); metaHolder = new MetaHolder(getApplicationContext()); // mediaPlayer.setOnCompletionListener(this); // restore state; currentPodcastInPlayer = 0; restoreState(); // remoteControlReceiver = new RemoteControlReceiver(this); // IntentFilter intentFilter = new IntentFilter(Intent.ACTION_MEDIA_BUTTON); // priority cribbed from // http://www.gauntface.co.uk/blog/2010/04/14/using-android-headset-buttons-earphone-buttons/ // intentFilter.setPriority(10*IntentFilter.SYSTEM_HIGH_PRIORITY); // registerReceiver(remoteControlReceiver, intentFilter); AudioManager mAudioManager = (AudioManager) getSystemService(AUDIO_SERVICE); ComponentName mMediaButtonReceiverComponent = new ComponentName(this, MusicIntentReceiver.class); mAudioManager.registerMediaButtonEventReceiver(mMediaButtonReceiverComponent); if (mRemoteControlClientCompat == null) { Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON); intent.setComponent(mMediaButtonReceiverComponent); mRemoteControlClientCompat = new RemoteControlClientCompat( PendingIntent.getBroadcast(this /*context*/, 0 /*requestCode, ignored*/, intent /*intent*/, 0 /*flags*/) ); RemoteControlHelper.registerRemoteControlClient(mAudioManager, mRemoteControlClientCompat); } mRemoteControlClientCompat.setPlaybackState( RemoteControlClient.PLAYSTATE_PAUSED); mRemoteControlClientCompat.setTransportControlFlags( RemoteControlClient.FLAG_KEY_MEDIA_PLAY | RemoteControlClient.FLAG_KEY_MEDIA_PAUSE | RemoteControlClient.FLAG_KEY_MEDIA_NEXT | RemoteControlClient.FLAG_KEY_MEDIA_STOP ); mDummyAlbumArt = BitmapFactory.decodeResource(getResources(), R.drawable.ccp_launcher); updateRemoteDisplay(); // foreground stuff try { mStartForeground = getClass().getMethod("startForeground", mStartForegroundSignature); mStopForeground = getClass().getMethod("stopForeground", mStopForegroundSignature); } catch (NoSuchMethodException e) { // Running on an older platform. mStartForeground = mStopForeground = null; try { mSetForeground = getClass().getMethod("setForeground", mSetForegroundSignature); } catch (NoSuchMethodException e1) { throw new IllegalStateException("OS doesn't have Service.startForeground OR Service.setForeground!"); } } // create the Audio Focus Helper, if the Audio Focus feature is available (SDK 8 or above) if (android.os.Build.VERSION.SDK_INT >= 8) mAudioFocusHelper = new AudioFocusHelper(getApplicationContext(), this); else mAudioFocus = AudioFocus.Focused; // no focus feature, so we always "have" audio focus } public void updateRemoteDisplay() { if (currentMeta() != null) { // Update the remote controls mRemoteControlClientCompat.editMetadata(true) .putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, currentMeta().getFeedName()) .putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, currentMeta().getTitle()) .putString(MediaMetadataRetriever.METADATA_KEY_TITLE, currentMeta().getDescription()) .putLong(MediaMetadataRetriever.METADATA_KEY_DURATION, currentMeta().getDuration()) .putBitmap( RemoteControlClientCompat.MetadataEditorCompat.METADATA_KEY_ARTWORK, mDummyAlbumArt) .apply(); } } @Override public int onStartCommand(Intent intent, int flags, int startId) { int not_sticky = Service.START_NOT_STICKY; Log.i("CarCast", "ContentService.onStartCommand()"); if (intent.getAction().equals(Intent.ACTION_MEDIA_BUTTON)) { KeyEvent keyEvent = (KeyEvent) intent.getExtras().get(Intent.EXTRA_KEY_EVENT); switch (keyEvent.getKeyCode()) { case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: pauseOrPlay(); break; case KeyEvent.KEYCODE_MEDIA_PLAY: if (!isPlaying()) { play(); } break; case KeyEvent.KEYCODE_MEDIA_PAUSE: case KeyEvent.KEYCODE_MEDIA_STOP: pauseNow(); break; case KeyEvent.KEYCODE_MEDIA_NEXT: bumpForwardSeconds(30); break; case KeyEvent.KEYCODE_MEDIA_PREVIOUS: bumpForwardSeconds(-30); break; } return not_sticky; } if (intent.getAction().equals(AudioManager.ACTION_AUDIO_BECOMING_NOISY)) { // if unplugged if (intent.getIntExtra("state", 0) == 0 && isPlaying()) { pauseNow(); bumpForwardSeconds(-2); if (playStatusListener != null) { playStatusListener.playStateUpdated(false); } } return not_sticky; } Bundle extras = intent.getExtras(); String external = extras.getString("external"); if (external == null) return not_sticky; Log.i("CarCast", "ContentService got intent with external extra:" + external); if (mediaPlayer == null) setApplicationContext(getApplicationContext()); if (external.equals(ExternalReceiver.PAUSE)) pauseNow(); if (external.equals(ExternalReceiver.PLAY) && !isPlaying()) play(); if (external.equals(ExternalReceiver.PAUSEPLAY)) pauseOrPlay(); return not_sticky; } @Override public void onDestroy() { super.onDestroy(); Log.i("CarCast", "ContentService destroyed"); // Toast.makeText(getApplication(), "Service Destroyed", 1000).show(); // Make sure our notification is gone. disableNotification(); giveUpAudioFocus(); AudioManager mAudioManager = (AudioManager) getSystemService(AUDIO_SERVICE); ComponentName mMediaButtonReceiverComponent = new ComponentName(this, MusicIntentReceiver.class); mAudioManager.unregisterMediaButtonEventReceiver(mMediaButtonReceiverComponent); } void giveUpAudioFocus() { if (mAudioFocus == AudioFocus.Focused && mAudioFocusHelper != null && mAudioFocusHelper.abandonFocus()) mAudioFocus = AudioFocus.NoFocusNoDuck; } public void pauseNow() { if (mediaPlayer.isPlaying()) { // Save current position currentMeta().setCurrentPos(mediaPlayer.getCurrentPosition()); currentMeta().save(); mediaPlayer.pause(); mediaMode = MediaMode.Paused; // say(activity, "paused " + currentTitle()); saveState(); mRemoteControlClientCompat.setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED); } disableNotification(); // activity.disableJumpButtons(); } /** * @returns playing or not */ public boolean pauseOrPlay() { try { if (mediaPlayer.isPlaying()) { pauseNow(); return false; } else { if (mediaMode == MediaMode.Paused) { enableNotification(); mediaPlayer.start(); mediaMode = MediaMode.Playing; // activity.enableJumpButtons(); mRemoteControlClientCompat.setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING); } else { play(); } return true; } } catch (Exception e) { Log.e("CarCast", "Unexpected exception", e); return false; } } private void play() { try { enableNotification(); if (!fullReset()) return; tryToGetAudioFocus(); // say(activity, "started " + currentTitle()); mediaPlayer.start(); mediaMode = MediaMode.Playing; saveState(); mRemoteControlClientCompat.setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING); } catch (Exception e) { TraceUtil.report(e); } } public void play(int position) { if (mediaPlayer.isPlaying()) { mediaPlayer.stop(); } currentPodcastInPlayer = position; play(); updateRemoteDisplay(); mRemoteControlClientCompat.setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING); } public void previous() { boolean playing = false; if (mediaPlayer.isPlaying()) { playing = true; mediaPlayer.stop(); // say(activity, "stopped " + currentTitle()); } mediaMode = MediaMode.UnInitialized; if (currentPodcastInPlayer > 0) { currentPodcastInPlayer--; updateRemoteDisplay(); } if (currentPodcastInPlayer >= metaHolder.getSize()) return; if (playing) play(); // final TextView textView = (TextView) activity // .findViewById(R.id.summary); // textView.setText(currentTitle()); } public void purgeHeard() { deleteUpTo(currentPodcastInPlayer); } public void resetToDemoSubscriptions() { subHelper.resetToDemoSubscriptions(); } public void restoreState() { final File stateFile = config.getPodcastRootPath("state.dat"); if (!stateFile.exists()) { location = null; return; } try { if (location == null) { location = Location.load(stateFile); } tryToRestoreLocation(); } catch (Throwable e) { // bummer. } } public void saveState() { try { final File stateFile = config.getPodcastRootPath("state.dat"); location = Location.save(stateFile, currentTitle()); } catch (Throwable e) { // bummer. } } public void setCurrentPaused(int position) { boolean wasPlaying = mediaPlayer.isPlaying(); if (wasPlaying) { currentMeta().setCurrentPos(mediaPlayer.getCurrentPosition()); mediaPlayer.stop(); } mediaMode = MediaMode.Paused; currentPodcastInPlayer = position; } public void setMediaMode(MediaMode mediaMode) { this.mediaMode = mediaMode; } // Synchronized: to ensure that maximum one startDownloadingNewPodCasts() // can be active at any time. // public synchronized void startDownloadingNewPodCasts(final int max) { if (downloadHelper != null && downloadHelper.isRunning()) { Log.w("carcast", "abort start - carcast already running"); return; } else { downloadHelper = new DownloadHelper(max); } Log.w("carcast", "startDownloadingNewPodCasts"); boolean autoDelete = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("autoDelete", false); if (autoDelete) { for (int i = metaHolder.getSize() - 1; i >= 0; i--) { MetaFile metaFile = metaHolder.get(i); if (currentTitle().equals(metaFile.getTitle())) { continue; } if (metaFile.getDuration() <= 0) { continue; } if (metaFile.isListenedTo()) { deletePodcast(i); } } } NotificationManager mNotificationManager = (NotificationManager) getSystemService(Activity.NOTIFICATION_SERVICE); mNotificationManager.cancel(22); updateNotification("Downloading podcasts started"); new Thread() { @Override public void run() { try { partialWakeLock.acquire(); Log.i("CarCast", "starting download thread."); // Lets not the phone go to sleep while doing // downloads.... PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "ContentService download thread"); WifiManager.WifiLock wifiLock = null; try { // The intent here is keep the phone from shutting // down during a download. wl.acquire(); // If we have wifi now, lets hold on to it. WifiManager wifi = (WifiManager) getSystemService(Context.WIFI_SERVICE); if (wifi.isWifiEnabled()) { wifiLock = wifi.createWifiLock("CarCast"); if (wifiLock != null) wifiLock.acquire(); Log.i("CarCast", "Locked Wifi."); } String accounts = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()).getString("accounts", "none"); downloadHelper.downloadNewPodCasts(ContentService.this); } finally { if (wifiLock != null) { try { wifiLock.release(); Log.i("CarCast", "released Wifi."); } catch (Throwable t) { Log.i("CarCast", "Yikes, issue releasing Wifi."); } } wl.release(); } } catch (Throwable t) { Log.i("CarCast", "Unpleasentness during download: " + t.getMessage()); } finally { Log.i("CarCast", "finished download thread."); partialWakeLock.release(); } } }.start(); } public String startSearch(String search) { if (search.equals("-status-")) { if (searchHelper.done) return "done"; return ""; } if (search.equals("-results-")) { return searchHelper.results; } searchHelper = new ItunesSearchHelper(search); //searchHelper = new SearchHelper(search); searchHelper.start(); return ""; } private void tryToRestoreLocation() { try { if (location == null) { return; } boolean found = false; for (int i = 0; i < metaHolder.getSize(); i++) { if (metaHolder.get(i).getTitle().equals(location.title)) { currentPodcastInPlayer = i; updateRemoteDisplay(); found = true; break; } } if (!found) { location = null; return; } mediaPlayer.reset(); mediaPlayer.setDataSource(currentFile().toString()); mediaPlayer.prepare(); mediaPlayer.seekTo(currentMeta().getCurrentPos()); mediaMode = MediaMode.Paused; } catch (Throwable e) { // bummer. } } void updateNotification(String update) { NotificationManager mNotificationManager = (NotificationManager) getSystemService(Activity.NOTIFICATION_SERVICE); Notification notification = new Notification(R.drawable.iconbusy, "Downloading started", System.currentTimeMillis()); PendingIntent contentIntent = PendingIntent.getActivity(this, 0, new Intent(this, CarCast.class), 0); notification.setLatestEventInfo(getBaseContext(), "Downloading Started", update, contentIntent); notification.flags = Notification.FLAG_ONGOING_EVENT; mNotificationManager.notify(23, notification); } public boolean isPlaying() { return mediaPlayer != null && mediaPlayer.isPlaying(); } public boolean isIdle() { return !isPlaying() && (downloadHelper == null || !downloadHelper.isRunning()); } public void purgeAll() { deleteUpTo(-1); } public String getDownloadProgress() { if (downloadHelper == null) return ""; return downloadHelper.sb.toString(); } public void purgeToCurrent() { deleteUpTo(currentPodcastInPlayer); } public void setPlayStatusListener(PlayStatusListener playStatusListener) { this.playStatusListener = playStatusListener; } public void newContentAdded() { metaHolder = new MetaHolder(getApplicationContext(), currentFile()); } public void directorySettingsChanged() { initDirs(); metaHolder = new MetaHolder(getApplicationContext(), currentFile()); } // This section cribbed from // http://developer.android.com/reference/android/app/Service.html#startForeground%28int,%20android.app.Notification%29 private static final Class<?>[] mSetForegroundSignature = new Class[]{boolean.class}; private static final Class<?>[] mStartForegroundSignature = new Class[]{int.class, Notification.class}; private static final Class<?>[] mStopForegroundSignature = new Class[]{boolean.class}; private Method mSetForeground; private Method mStartForeground; private Method mStopForeground; private Object[] mSetForegroundArgs = new Object[1]; private Object[] mStartForegroundArgs = new Object[2]; private Object[] mStopForegroundArgs = new Object[1]; void invokeMethod(Method method, Object[] args) { try { mStartForeground.invoke(this, mStartForegroundArgs); } catch (InvocationTargetException e) { // Should not happen. Log.w("CarCast-ContentService", "Unable to invoke method", e); } catch (IllegalAccessException e) { // Should not happen. Log.w("CarCast-ContentService", "Unable to invoke method", e); } } /** * This is a wrapper around the new startForeground method, using the older APIs if it is not available. */ void startForegroundCompat(int id, Notification notification) { // If we have the new startForeground API, then use it. if (mStartForeground != null) { mStartForegroundArgs[0] = Integer.valueOf(id); mStartForegroundArgs[1] = notification; invokeMethod(mStartForeground, mStartForegroundArgs); return; } // Fall back on the old API. mSetForegroundArgs[0] = Boolean.TRUE; invokeMethod(mSetForeground, mSetForegroundArgs); } /** * This is a wrapper around the new stopForeground method, using the older APIs if it is not available. */ void stopForegroundCompat() { // If we have the new stopForeground API, then use it. if (mStopForeground != null) { mStopForegroundArgs[0] = Boolean.TRUE; try { mStopForeground.invoke(this, mStopForegroundArgs); } catch (InvocationTargetException e) { // Should not happen. Log.w("CarCast-ContentService", "Unable to invoke stopForeground", e); } catch (IllegalAccessException e) { // Should not happen. Log.w("CarCast-ContentService", "Unable to invoke stopForeground", e); } return; } // Fall back on the old API. Note to cancel BEFORE changing the // foreground state, since we could be killed at that point. mSetForegroundArgs[0] = Boolean.FALSE; invokeMethod(mSetForeground, mSetForegroundArgs); } private WakeLock partialWakeLock; void enableNotification() { PendingIntent contentIntent = PendingIntent.getActivity(this, 0, new Intent(this, CarCast.class), 0); Notification notification = new Notification(R.drawable.ccp_launcher, null, System.currentTimeMillis()); notification.flags |= Notification.FLAG_ONGOING_EVENT | Notification.FLAG_NO_CLEAR; notification.setLatestEventInfo(this, getText(R.string.notification_status), getText(R.string.notification_text), contentIntent); startForegroundCompat(R.string.notification_status, notification); partialWakeLock.acquire(); } void disableNotification() { stopForegroundCompat(); partialWakeLock.release(); } public SortedSet<Integer> moveTop(SortedSet<Integer> checkedItems) { return metaHolder.moveTop(checkedItems); } public SortedSet<Integer> moveUp(SortedSet<Integer> checkedItems) { return metaHolder.moveUp(checkedItems); } public SortedSet<Integer> moveBottom(SortedSet<Integer> checkedItems) { return metaHolder.moveBottom(checkedItems); } public SortedSet<Integer> moveDown(SortedSet<Integer> checkedItems) { return metaHolder.moveDown(checkedItems); } public void exportOPML(FileOutputStream fileOutputStream) { ExportOpml.export(getSubscriptions(), fileOutputStream); } Thread publishRecordingsThread; public void publishRecordings(final Activity activity) { if (publishRecordingsThread != null) { return; } publishRecordingsThread = new Thread() { @Override public void run() { try { partialWakeLock.acquire(); Log.i("CarCast", "starting recording thread."); // Lets not the phone go to sleep while doing // downloads.... PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "ContentService download thread"); WifiManager.WifiLock wifiLock = null; try { // The intent here is keep the phone from shutting // down during a download. wl.acquire(); // If we have wifi now, lets hold on to it. WifiManager wifi = (WifiManager) getSystemService(Context.WIFI_SERVICE); if (wifi.isWifiEnabled()) { wifiLock = wifi.createWifiLock("CarCast"); if (wifiLock != null) wifiLock.acquire(); Log.i("CarCast", "recording Locked Wifi."); } Thread.sleep(2000); // Give wifi two seconds to stablize MailRecordings.doIt(ContentService.this); } finally { if (wifiLock != null) { try { wifiLock.release(); Log.i("CarCast", "recording released Wifi."); } catch (Throwable t) { Log.i("CarCast", "recording Yikes, issue releasing Wifi."); } } wl.release(); } } catch (javax.mail.AuthenticationFailedException afe) { doWarning(activity, "Problem authenticating. Check username/password."); } catch (final Throwable t) { Log.i("CarCast", "Unpleasentness during email of recording: " + t.getMessage() + " " + t.getClass()); String msg = t.getMessage(); if (msg == null) { msg = t.toString(); } doWarning(activity, msg); } finally { Log.i("CarCast", "finished recording post thread."); partialWakeLock.release(); publishRecordingsThread = null; } } public void doWarning(Activity activity, final String message) { if (activity != null) { activity.runOnUiThread(new Runnable() { public void run() { Toast.makeText(getApplication(), "Error sending email: " + message, Toast.LENGTH_LONG).show(); } }); } } }; publishRecordingsThread.start(); } /** * Signals that audio focus was gained. */ public void onGainedAudioFocus() { //Toast.makeText(getApplicationContext(), "CarCast: gained audio focus.", Toast.LENGTH_SHORT).show(); mAudioFocus = AudioFocus.Focused; if (mPauseReason == PauseReason.FocusLoss) { mPauseReason = PauseReason.UserRequest; play(); } } /** * Signals that audio focus was lost. * * @param canDuck If true, audio can continue in "ducked" mode (low volume). Otherwise, all * audio must stop. */ public void onLostAudioFocus(boolean canDuck) { // Toast.makeText(getApplicationContext(), "lost audio focus." + (canDuck ? "can duck" : // "no duck"), Toast.LENGTH_SHORT).show(); mAudioFocus = canDuck ? AudioFocus.NoFocusCanDuck : AudioFocus.NoFocusNoDuck; if (isPlaying()) { mPauseReason = PauseReason.FocusLoss; pauseNow(); bumpForwardSeconds(-3); } } void tryToGetAudioFocus() { if (mAudioFocus != AudioFocus.Focused && mAudioFocusHelper != null && mAudioFocusHelper.requestFocus()) mAudioFocus = AudioFocus.Focused; } }